#include "GeodesicSphere.h"
#include <QGLShaderProgram>
#include <cmath>
#include <QDebug>

GeodesicSphere::GeodesicSphere()
  : mPosition(0,0,0),
    mRadius(1.0),
    mMaxEdgeLength(1.0),
    mDisplayListID(0),
    mColor(255,255,255,255),
    mShader(NULL),
    mGenerateTextureCoords(true)
{
}

GeodesicSphere::GeodesicSphere(const QVector3D &pos, float radius, float maxEdgeLength)
  : mPosition(pos),
    mRadius(radius),
    mMaxEdgeLength(maxEdgeLength),
    mDisplayListID(0),
    mColor(255,255,255,255),
    mShader(NULL),
    mGenerateTextureCoords(true)
{
}

GeodesicSphere::~GeodesicSphere()
{
  if (glIsList(mDisplayListID))
    glDeleteLists(mDisplayListID, 1);
}

const QVector3D& GeodesicSphere::pos() const
{
  return mPosition;
}

void GeodesicSphere::setSize(float size)
{
  mRadius = size;
}

void GeodesicSphere::setPos(const QVector3D& pos)
{
  mPosition = pos;
}

void GeodesicSphere::setColor(const QColor &color)
{
  mColor = color;
}

void GeodesicSphere::setShader(QGLShaderProgram *shader)
{
  mShader = shader;
}

void GeodesicSphere::generateTextureCoords(bool generateTextureCoords)
{
  mGenerateTextureCoords = generateTextureCoords;
}

void GeodesicSphere::create()
{
  const float sqrt5(sqrt(5.0));
  const float phi((1.0+sqrt5)*0.5);
  const float cRadius(sqrt(10.0+(2.0*sqrt5))/(4.0*phi));
  double a = (mRadius/cRadius) * 0.5;
  double b = (mRadius/cRadius) / (2.0*phi);

  QVector3D
       v1(mPosition+QVector3D( 0, b,-a)),
       v2(mPosition+QVector3D( b, a, 0)),
       v3(mPosition+QVector3D(-b, a, 0)),
       v4(mPosition+QVector3D( 0, b, a)),
       v5(mPosition+QVector3D( 0,-b, a)),
       v6(mPosition+QVector3D(-a, 0, b)),
       v7(mPosition+QVector3D( 0,-b,-a)),
       v8(mPosition+QVector3D( a, 0,-b)),
       v9(mPosition+QVector3D( a, 0, b)),
      v10(mPosition+QVector3D(-a, 0,-b)),
      v11(mPosition+QVector3D( b,-a, 0)),
      v12(mPosition+QVector3D(-b,-a, 0));

  //if (glIsList(mDisplayListID))
  //  glDeleteLists(mDisplayListID, 1);

  //mDisplayListID = glGenLists(1);
  //glNewList(mDisplayListID, GL_COMPILE);
    subdivideThenRenderTriangle( v1, v2, v3);
    subdivideThenRenderTriangle( v4, v3, v2);
    subdivideThenRenderTriangle( v4, v5, v6);
    subdivideThenRenderTriangle( v4, v9, v5);
    subdivideThenRenderTriangle( v1, v7, v8);
    subdivideThenRenderTriangle( v1,v10, v7);
    subdivideThenRenderTriangle( v5,v11,v12);
    subdivideThenRenderTriangle( v7,v12,v11);
    subdivideThenRenderTriangle( v3, v6,v10);
    subdivideThenRenderTriangle(v12,v10, v6);
    subdivideThenRenderTriangle( v2, v8, v9);
    subdivideThenRenderTriangle(v11, v9, v8);
    subdivideThenRenderTriangle( v4, v6, v3);
    subdivideThenRenderTriangle( v4, v2, v9);
    subdivideThenRenderTriangle( v1, v3,v10);
    subdivideThenRenderTriangle( v1, v8, v2);
    subdivideThenRenderTriangle( v7,v10,v12);
    subdivideThenRenderTriangle( v7,v11, v8);
    subdivideThenRenderTriangle( v5,v12, v6);
    subdivideThenRenderTriangle( v5, v9,v11);
  //glEndList();
}

void GeodesicSphere::drawTriangle(const QVector3D &a, const QVector3D &b, const QVector3D &c)
{
  QVector3D anormal(a-mPosition),
            bnormal(b-mPosition),
            cnormal(c-mPosition);
  QVector3D p1(cnormal.normalized()),
            p2(bnormal.normalized()),
            p3(anormal.normalized());

  float s1(atan2(p1.x(),p1.z())/(2*3.14159)+0.5),
        t1(asin(p1.y())/3.14159+0.5),
        s2(atan2(p2.x(),p2.z())/(2*3.14159)+0.5),
        t2(asin(p2.y())/3.14159+0.5),
        s3(atan2(p3.x(),p3.z())/(2*3.14159)+0.5),
        t3(asin(p3.y())/3.14159+0.5);

  // Check to see if the triangle exists on a seam.  This technically should use some function of
  // mMaxEdgeLength, instead of 0.5, but this should work for most.
  if (fabs(s1-s2) > 0.5 ||
      fabs(s2-s3) > 0.5 ||
      fabs(s3-s1) > 0.5)
  {
    if (s1 < 0.5) s1 += 1;
    if (s2 < 0.5) s2 += 1;
    if (s3 < 0.5) s3 += 1;
  }

  float u1 = s1, u2 = s2, u3 = s3,
        v1 = t1, v2 = t2, v3 = t3;
  float d = 1.0/((u2-u1)*(v3-v1)-(u3-u1)*(v2-v1));

  QVector3D   tangent(((p2-p1)*(v3-v1)+(p3-p1)*(v1-v2))*d),
            bitangent(((p2-p1)*(u1-u3)+(p3-p1)*(u2-u1))*d);

  glBegin(GL_TRIANGLES);
    glTexCoord2f(s1, t1);
    if (mShader != NULL)
    {
      mShader->setAttributeValue(6, tangent);
      mShader->setAttributeValue(7, bitangent);
    }
    glColor3f(mColor.red(), mColor.green(), mColor.blue());
    glNormal3f(cnormal.x(), cnormal.y(), cnormal.z());
    if (mGenerateTextureCoords)
        glTexCoord2f(s1, t1);
    glVertex3f(c.x(), c.y(), c.z());
    glNormal3f(bnormal.x(), bnormal.y(), bnormal.z());
    if (mGenerateTextureCoords)
      glTexCoord2f(s2, t2);
    glVertex3f(b.x(), b.y(), b.z());
    glNormal3f(anormal.x(), anormal.y(), anormal.z());
    if (mGenerateTextureCoords)
      glTexCoord2f(s3, t3);
    glVertex3f(a.x(), a.y(), a.z());
  glEnd();
}

QVector3D GeodesicSphere::midpointOnSphere(const QVector3D &a, const QVector3D &b)
{
  QVector3D midpoint((a+b)/2.0),
           unitRadial(midpoint-mPosition);
  unitRadial.normalize();
  return mPosition + (unitRadial*mRadius);
}

void GeodesicSphere::subdivideThenRenderTriangle(const QVector3D &a, const QVector3D &b, const QVector3D &c)
{
  QVector3D edge1(a-b), edge2(b-c), edge3(c-a);
  float mag1(edge1.length()), mag2(edge2.length()), mag3(edge3.length());
  if (mag1 < mMaxEdgeLength && mag2 < mMaxEdgeLength && mag3 < mMaxEdgeLength)
  {
    drawTriangle(a, b, c);
  }
  else
  {
    QVector3D ab(midpointOnSphere(a, b)),
        bc(midpointOnSphere(b, c)),
        ca(midpointOnSphere(c, a));
    subdivideThenRenderTriangle(a, ab, ca);
    subdivideThenRenderTriangle(ab, b, bc);
    subdivideThenRenderTriangle(ca, bc, c);
    subdivideThenRenderTriangle(ab, bc, ca);
  }
}

QVector2D GeodesicSphere::latLonFromPoint(const QVector3D &point) const
{
  return QVector2D();
}

void GeodesicSphere::render()
{
  create();
  //glCallList(mDisplayListID);
}
